/*
 * // Bristleback plugin - Copyright (c) 2010 bristleback.googlecode.com
 * // ---------------------------------------------------------------------------
 * // This program is free software; you can redistribute it and/or modify it
 * // under the terms of the GNU Lesser General Public License as published by the
 * // Free Software Foundation; either version 3 of the License, or (at your
 * // option) any later version.
 * // This library is distributed in the hope that it will be useful,
 * // but without any warranty; without even the implied warranty of merchantability
 * // or fitness for a particular purpose.
 * // You should have received a copy of the GNU Lesser General Public License along
 * // with this program; if not, see <http://www.gnu.org/licenses/lgpl.html>.
 * // ---------------------------------------------------------------------------
 */

package pl.bristleback.server.bristle.binding;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import pl.bristleback.server.bristle.actions.RemoteAction;
import pl.bristleback.server.bristle.binding.resolvers.BindingEngine;
import pl.bristleback.server.bristle.binding.resolvers.PropertyValueResolver;
import pl.bristleback.server.bristle.config.BristleConfiguration;
import pl.bristleback.server.bristle.exceptions.BristleRuntimeException;
import pl.bristleback.server.bristle.utils.ActionClassElement;
import pl.bristleback.server.bristle.utils.ReflectionUtil;
import pl.bristleback.server.bristle.utils.StringUtil;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * It is a default bindings processor.
 * If binding annotation has not filled root name,
 * this processor sets root element name equal to processed object binder field name.
 * Token variable with root element name as key will be chosen by binding engine in binding operation.
 * <p/>
 * Created on: 2011-02-20 21:27:21 <br/>
 *
 * @author Wojciech Niemiec
 */
public class DefaultBindingsProcessor implements BindingsProcessor {
  private static Logger log = Logger.getLogger(DefaultBindingsProcessor.class.getName());

  private static Map<Class, PropertyValueResolver> nonDefaultValueResolvers = new HashMap<Class, PropertyValueResolver>();
  private static Map<Class, BindingEngine> bindingEngines = new HashMap<Class, BindingEngine>();

  @SuppressWarnings("unchecked")
  public List<ActionClassElement<ObjectBinder>> processBindings(Class<? extends RemoteAction> actionClass, BristleConfiguration configuration) {
    List<Field> binderFields = ReflectionUtil.getDependencyFieldsWithType(actionClass, ObjectBinder.class, true);
    List<ActionClassElement<ObjectBinder>> binders = new ArrayList<ActionClassElement<ObjectBinder>>();
    for (Field binderField : binderFields) {
      ParameterizedType classType = (ParameterizedType) binderField.getGenericType();
      Class clazz = (Class) classType.getActualTypeArguments()[0];
      ObjectBinder objectBinder = new ObjectBinder();
      objectBinder.setBeanClass(clazz);
      Bind bindingAnnotation = binderField.getAnnotation(Bind.class);
      if (bindingAnnotation != null) {
        processBindingAnnotation(objectBinder, binderField, bindingAnnotation, configuration);
      }

      ActionClassElement<ObjectBinder> binderElement = createBinderElement(actionClass, binderField, objectBinder);
      binders.add(binderElement);
    }
    return binders;
  }

  private void processBindingAnnotation(ObjectBinder objectBinder, Field binderField, Bind bindingAnnotation, BristleConfiguration configuration) {
    String rootName = getRootName(binderField, bindingAnnotation);
    NestedFieldInformation root = NestedFieldInformation.createRoot(objectBinder.getBeanClass(), rootName);
    for (Property property : bindingAnnotation.properties()) {
      processPropertyBinding(root, property, property.name(), configuration);
    }
    objectBinder.setRootBean(root);
    setEngine(objectBinder, bindingAnnotation, configuration);
  }

  private String getRootName(Field binderField, Bind bindingAnnotation) {
    if (StringUtils.isNotEmpty(bindingAnnotation.rootName())) {
      return bindingAnnotation.rootName();
    }
    return binderField.getName();
  }

  private void processPropertyBinding(NestedFieldInformation parent, Property property, String propertyPartName, BristleConfiguration configuration) {
    String[] propertyNames = propertyPartName.split("\\" + StringUtil.DOT);
    String childName = propertyNames[0];
    boolean isIntermediary = propertyNames.length > 1;

    NestedFieldInformation child = parent.getChild(childName);
    if (child == null) {
      // this property must be processed.
      child = createChild(parent, property, childName, isIntermediary);
    }
    if (isIntermediary) {
      // this child is intermediary and has its own children that must be processed.
      setRequired(property, child);
      String childPropertyNames = propertyPartName.substring(propertyPartName.indexOf(StringUtil.DOT) + 1);
      processChildProperties(property, child, childPropertyNames, configuration);
    } else if (child.isContainer()) {
      // this child must be array/list/map of raw types.
      setValueResolver(child.getElementFieldInformation(), property, configuration);
    } else {
      // this child must be of row type
      setValueResolver(child, property, configuration);
    }
  }

  private void processChildProperties(Property property, NestedFieldInformation child, String childPropertyNames, BristleConfiguration configuration) {
    if (child.getElementFieldInformation() != null) {
      processPropertyBinding(child.getElementFieldInformation(), property, childPropertyNames, configuration);
    } else {
      processPropertyBinding(child, property, childPropertyNames, configuration);
    }
  }

  private NestedFieldInformation createChild(NestedFieldInformation parent, Property property, String childName, boolean isIntermediary) {
    NestedFieldInformation child = parent.addChild(childName);
    getGetterAndSetter(parent, child, childName);
    child.setIntermediary(isIntermediary);
    child.setFieldClass(child.getGetter().getReturnType());
    setRequired(property, child);
    setContainerInPath(parent, child);
    assignContainerElementFieldInformation(child);
    return child;
  }

  private void setContainerInPath(NestedFieldInformation parent, NestedFieldInformation child) {
    if (parent.isContainerInPath()) {
      child.setContainerInPath(true);
    } else {
      child.setContainerInPath(child.isContainer());
    }
  }

  private void setRequired(Property property, NestedFieldInformation child) {
    if (!child.isContainer() || !child.isIntermediary()) {
      if (property.required()) {
        // this prevents changing "required" flag from true to false, once field is required, it must remain required.
        child.setRequired(property.required());
      }
    } else {
      child.setRequired(false);
    }
  }

  private void assignContainerElementFieldInformation(NestedFieldInformation fieldInformation) {
    Class clazz = fieldInformation.getFieldClass();
    Class containerElementType;
    if (clazz.isArray()) {
      containerElementType = clazz.getComponentType();
    } else if (Collection.class.isAssignableFrom(clazz)) {
      Type[] genericType = clazz.getGenericInterfaces();
      if (genericType[0] instanceof ParameterizedType) {
        containerElementType = (Class) ((ParameterizedType) (fieldInformation.getGetter().getGetterMethod().getGenericReturnType())).getActualTypeArguments()[0];
      } else {
        containerElementType = String.class;
      }
    } else if (Map.class.isAssignableFrom(clazz)) {
      //    Type genericType = clazz.getGenericSuperclass();
      //    if (clazz instanceof ParameterizedType) {
      Type genericReturnType = fieldInformation.getGetter().getGetterMethod().getGenericReturnType();
      if (!(genericReturnType instanceof Class)) {
        containerElementType = (Class) ((ParameterizedType) (fieldInformation.getGetter().getGetterMethod().getGenericReturnType())).getActualTypeArguments()[1];
      } else {
        containerElementType = String.class;
      }
    } else {
      containerElementType = null;
    }
    if (containerElementType != null) {
      NestedFieldInformation containerElementInformation = new NestedFieldInformation();
      containerElementInformation.setFieldClass(containerElementType);
      containerElementInformation.setIntermediary(fieldInformation.isIntermediary());
      containerElementInformation.setRequired(fieldInformation.isRequired());
      containerElementInformation.setName(fieldInformation.getName());
      containerElementInformation.setFullPathName(fieldInformation.getFullPathName());
      containerElementInformation.setRoot(fieldInformation.getRoot());
      fieldInformation.setElementFieldInformation(containerElementInformation);
      containerElementInformation.setContainerInPath(fieldInformation.isContainerInPath());
    }
  }

  private void getGetterAndSetter(NestedFieldInformation parent, NestedFieldInformation child, String childName) {
    child.setGetter(ReflectionUtil.getGetterMethod(parent.getFieldClass(), childName, true));
    child.setSetter(ReflectionUtil.getSetterMethod(parent.getFieldClass(), childName, true));
  }

  private void setEngine(ObjectBinder objectBinder, Bind bindingAnnotation, BristleConfiguration configuration) {
    BindingEngine engine;
    if (bindingAnnotation.engine().equals(BindingEngine.class)) {
      engine = configuration.getDefaultBindingEngine();
    } else {
      engine = loadCustomEngine(bindingAnnotation);
    }
    objectBinder.setEngine(engine);
  }

  private BindingEngine loadCustomEngine(Bind bindingAnnotation) {
    BindingEngine engine = bindingEngines.get(bindingAnnotation.engine());
    if (engine == null) {
      engine = createBindingEngine(bindingAnnotation);
      bindingEngines.put(bindingAnnotation.engine(), engine);
    }
    return engine;
  }

  private BindingEngine createBindingEngine(Bind bindingAnnotation) {
    try {
      return bindingAnnotation.engine().newInstance();
    } catch (InstantiationException e) {
      throw new BristleRuntimeException("Cannot create instance of binding engine. " + e.getMessage(), e);
    } catch (IllegalAccessException e) {
      throw new BristleRuntimeException("Cannot create instance of binding engine. " + e.getMessage(), e);
    }
  }

  private void setValueResolver(NestedFieldInformation fieldBindingInformation, Property propertyBinding, BristleConfiguration configuration) {
    Class fieldType = fieldBindingInformation.getFieldClass();
    if (propertyBinding.valueResolver().equals(PropertyValueResolver.class)) {
      loadDefaultValueResolver(fieldBindingInformation, fieldType, configuration);
    } else {
      loadNonDefaultValueResolver(fieldBindingInformation, propertyBinding);
    }
  }

  private void loadNonDefaultValueResolver(NestedFieldInformation fieldBindingInformation, Property propertyBinding) {
    try {
      PropertyValueResolver resolver = nonDefaultValueResolvers.get(propertyBinding.valueResolver());
      if (resolver == null) {
        resolver = propertyBinding.valueResolver().newInstance();
        nonDefaultValueResolvers.put(propertyBinding.valueResolver(), resolver);
      }
      fieldBindingInformation.setResolver(resolver);
    } catch (InstantiationException e) {
      throw new BristleRuntimeException("Cannot create instance of property value resolver. " + e.getMessage(), e);
    } catch (IllegalAccessException e) {
      throw new BristleRuntimeException("Cannot create instance of property value resolver. " + e.getMessage(), e);
    }
  }

  private void loadDefaultValueResolver(NestedFieldInformation fieldBindingInformation, Class fieldType, BristleConfiguration configuration) {
    PropertyValueResolver resolver = configuration.getDefaultValueResolver(fieldType);
    fieldBindingInformation.setResolver(resolver);
  }

  private ActionClassElement<ObjectBinder> createBinderElement(Class<? extends RemoteAction> actionClass, Field binderField, ObjectBinder objectBinder) {
    ActionClassElement<ObjectBinder> binderElement = new ActionClassElement<ObjectBinder>();
    binderElement.setOwnerClass(actionClass);
    binderElement.setElementField(binderField);
    binderElement.setElementValue(objectBinder);

    return binderElement;
  }
}
